import datetime
from typing import Set

import humps
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer
from sqlalchemy import MetaData

__all__ = ('Base', 'ModelBase', 'MetaData')


convention = {
    "ix": 'ix_%(column_0_label)s',
    "uq": "uq_%(table_name)s_%(column_0_name)s",
    "ck": "ck_%(table_name)s_%(constraint_name)s",
    "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
    "pk": "pk_%(table_name)s"
}
MetaData = MetaData(naming_convention=convention)
Base = declarative_base(metadata=MetaData)


class ModelBase(Base):
    __abstract__ = True
    _static_fields = ('id', 'create_ts', 'update_ts')
    _exclude_fields = ()

    id = Column(Integer, primary_key=True, autoincrement=True)
    create_ts = Column(Integer, default=lambda: datetime.datetime.now().timestamp())
    update_ts = Column(Integer, onupdate=lambda: datetime.datetime.now().timestamp(),
                       default=lambda: datetime.datetime.now().timestamp())

    def __str__(self):
        return f'<{self.__class__.__name__}:{self.id}>'

    def __repr__(self):
        return self.__str__()

    @classmethod
    def serialization_fields(cls) -> Set:
        return {col.key for col in cls.__table__.columns if not col.key.startswith('_')}

    @classmethod
    def new_from_object(cls, obj, extension=None, hump=False):
        if not obj:
            return cls()
        keys = dir(obj)
        items = {}
        for key in keys:
            if key.startswith('_'):
                continue
            if hump:
                items[key] = getattr(obj, key)
            items[humps.decamelize(key)] = getattr(obj, key)
        return cls.new_from_dict(items, extension=extension, hump=False)

    @classmethod
    def new_from_dict(cls, items: dict, extension: dict = None, hump=False):
        if not items:
            return cls()
        if hump:
            de_humps_items = dict()
            for key in items.keys():
                de_humps_items[humps.decamelize(key)] = items[key]
            items = de_humps_items

        kwargs = {}
        for column in cls.__table__.columns:
            if extension and column.key in extension:
                kwargs[column.key] = extension[column.key]
            elif column.key in items:
                kwargs[column.key] = items[column.key]
        return cls(**kwargs)

    def update_from_object(self, obj, extension: dict = None, hump=False):
        keys = dir(obj) if obj else []
        items = {}
        for key in keys:
            if key.startswith('_'):
                continue
            if hump:
                items[key] = getattr(obj, key)
            items[humps.decamelize(key)] = getattr(obj, key)
        return self.update_from_dict(items, extension=extension, hump=False)

    def update_from_dict(self, items: dict, extension: dict = None, hump=False):
        if hump:
            items = dict(items)
            for key in items.keys():
                items[humps.decamelize(key)] = items[key]

        for column in self.__class__.__table__.columns:
            if column in self._static_fields:
                continue
            if extension and column.key in extension:
                value = extension[column.key]
            elif column.key in items:
                value = items[column.key]
            else:
                continue
            if getattr(self, column.key) != value:
                setattr(self, column.key, value)

    def to_dict(self, include_fields=(), exclude_fields=(), hump=False):
        if not include_fields and not exclude_fields:
            exclude_fields = self._exclude_fields
        all_fields = self.serialization_fields()
        include_fields = all_fields & set(include_fields) if include_fields else all_fields
        exclude_fields = set(exclude_fields) if exclude_fields else set()
        fields = include_fields - exclude_fields
        result = dict()
        for field in fields:
            key = humps.camelize(field) if hump else field
            value = getattr(self, field, None)
            if isinstance(value, (datetime.date, datetime.datetime)):
                value = value.strftime('%Y-%m-%dT%H:%M:%S')
            result[key] = value
        return result
